<?

/** HTML parsing class is one of the source parsers. They all are called
 *  dzParser and they all share one common public method: parse(). This
 *  method returns necessary string for dzStruct.
 *
 *  This collection of functions (libary) is licensed under GPL2.
 *  See LICENSE or www.gnu.org for more details.
 *
 *  $Id: dzinhtml.php 595 2006-04-03 16:00:47Z jyry $
 */


/** HTML entity tags used with dataz */
define(HTML_ENTITY_TAG, "HTML");        /** Html */
define(TABLE_ENTITY_TAG, "TABLE");      /** Table */
define(HEAD_ENTITY_TAG, "HEAD");        /** Head section */
define(BODY_ENTITY_TAG, "BODY");        /** Body section */
define(THEAD_ENTITY_TAG, "THEAD");      /** Tablehead section */
define(TBODY_ENTITY_TAG, "TBODY");      /** Tablebody section */
define(TR_ENTITY_TAG, "TR");            /** Table row */
define(TH_ENTITY_TAG, "TH");            /** Table heading cell */
define(TD_ENTITY_TAG, "TD");            /** Table cell */
define(TABLE_LEVEL, 2);                 /** Table entity level */
define(TD_LEVEL, 5);                    /** TD/TH entity level */


/** Other, misc. constants */
define(DZ_TD_PREFIX, "DZ");             /** prefix to identify dz entities
                                         *  from other html ones */
define(DZ_TD_PREFIX_DELIM, "_");        /** delimiter to separate dz entity
                                         *  prefix from dz entity no:
                                         *  xyz where x=prefix, y=delimiter
                                         *  and z=no e.g. DZ_3 ("DZ"=prefix,
                                         *  "_"=delim and 3=no)
                                         */

include_once("sobaseclass.php");
include_once("sostring.php");
include_once("sostreams.php");
include_once("soset.php");
include_once("dzconst.php");
include_once("dzgeneric.php");
include_once("dzsrcentity.php");
include_once("dzgeneric.php");


/** Class to import HTML template and set up a dz form accordingly.
 *  This class reads in a html __Source (soInputStream) and breaks
 *  it into tables, rows and cells with all the html formatting
 *  information. It can also output it to the browser in original
 *  form or modified.
 *  @var dzSECache $__Cache Source entity cache
 *  @var soInputStream $__Source Html file or html code
 *  @var int $__NextIndexNo Next indexno. to be used in
 *      $this->__Cache->Entities
 *  @var array/string $__ENTITY_TYPES List of entity tags that should be
 *      parsed and stored in Cache->Entities
 *  @var array/string $__PageSources An array of all page source file names.
 *  @var array/string $__SubPageSources Tmp array -- this should be get sird of
 *  @var soIni $__ColsIni Temporary storage for dzcols section.
 */
class dzParser extends soBaseClass {

    /* @access private */
    var $__Cache;
    var $__Source;
    var $__NextIndexNo;

    var $__ENTITY_TYPES;
    var $__PageSources = array();
    var $__SubPageSources = array();
    var $__ColsIni;


    /** constructor
     *  Calls a html parser of this class.
     *  @param string $SourceStream Main page stream input ("file://file.html"
     *      or actual html input as a string)
     */
    function dzParser($SourceStream) {
        $this->sobaseClass();

        /*  ENTITY_TYPES
            Two dim array of all levels and entity types. Level is simple
            telling which entities can be on the same "level" and which are
            expected to be inside the upper levels.

            (This should really go to defines, but unfortunately PHP does not
            support array constants.)

            Cache->CellNos can also contain upper levels and therefore they
            are parsed until no entities are left inside the other.

            Level 0:    HTML
            Level 1:    HEAD and BODY
            Level 2:    TABLE
            Level 3:    THEAD and TBODY
            Level 4:    TR
            Level 5:    TH and TD
         */
        $this->__ENTITY_TYPES = array(
                                array(HTML_ENTITY_TAG),
                                array(HEAD_ENTITY_TAG, BODY_ENTITY_TAG),
                                array(TABLE_ENTITY_TAG),
                                array(THEAD_ENTITY_TAG, TBODY_ENTITY_TAG),
                                array(TR_ENTITY_TAG),
                                array(TH_ENTITY_TAG, TD_ENTITY_TAG)
                                );
        $this->__Cache = new dzSECache();
        $this->__ColsIni = new soIni;

        if (soGetStreamType($SourceStream) == "file") {
            $file_name = soExtractVal($SourceStream, "://", 2);
            $str_name = basename($file_name);
            $this->__PageSources = array($str_name);
            $str_name = soParseFileName(basename($file_name));
            $this->__Cache->StructName = $str_name;
            /* Insert the form source path into source stream */
        }; // if

        $this->__Source = &new soInputStream($SourceStream);
        if ($this->__Source->Error) {
            dzSysError("Could not find form source file $SourceStream");
        } else {
            soDebug("new dzInHtml created", SOD_DET);
        } // if

        $this->Error = $this->__Source->Error;
    } // constructor


    /** Main function that parses HTML code
     *  @param int $PageNo Page no. to parse - other values than 0 are used
     *      only by the function itself (recursively).
     *  @return dzSECache Cache object.
     */
    function parse($PageNo = 0) {
        if ($PageNo) {
            if (count($this->__Cache->Pages) <= $PageNo) {
                soDebug("dzInHtml->Parse($PageNo) ERROR - page no. '$PageNo' ".
                        "larger than max '".
                        count($this->__Cache->Pages)."'", SOD_HI);
                return 0; // <-- EXIT
            };
        } else {
            $this->__Cache->addPage();
            soDebug("dzInHtml->Parse($PageNo) start", SOD_DET);
        };

        $page = &$this->__Cache->Pages[$PageNo];

        /* Title of the page is important for human readability, and it is
         * used in menus, subari etc. */
        $page->Title = trim(soParseVal($this->__Source->Data,
                                               "<TITLE>",
                                               "</TITLE>", 1, 1));
        if (!$page->Title) {
            /* Use structname if title not spefified */
            $page->Title = $page->StructName;
        };

        /* Head section for the struct */
        $this->__processDataxiBlock($PageNo);

        /* look for HTML tags */
        $start = soSplitStr($this->__Source->Data,
                            "<".$this->__ENTITY_TYPES[0][0], 1, 1, 1);
        $end = soLocateStr($this->__Source->Data,
                           "</".$this->__ENTITY_TYPES[0][0], 1, 1);

        if (!$start[0] || ($end == -1)) {
            soDebug("dzInHtml->Parse(".$PageNo.
                    ") ERROR: no HTML tags found!", SOD_HI);
            $this->Error = 1;
            return $this->__Cache; // <-- EXIT
        };

        $entity = new dzSourceEntity();
        $entity->PreCode = substr($start[0], 0, strlen($start[0])
                                      -strlen($this->__ENTITY_TYPES[0][0]));
        $start = soSplitStr($start[1], ">", 1, 0, 0);
        $entity->setProperties($start[0]);
        $start = soSplitStr($start[1],
                                "</".$this->__ENTITY_TYPES[0][0], 1, 2, 1);
        $entity->Value = $start[0];
        $entity->PostCode = $start[1];
        $entity->Type = $this->__ENTITY_TYPES[0][0];
        $entity->Level = 0;
        $entity->IndexNo = 0;
        $page->Entities[] = $entity;
        $this->__NextIndexNo = 1;

        /* loopy doo - all entity levels to go through  */
        $ecount = count($this->__ENTITY_TYPES);
        soDebug("dzParser->parse(".$PageNo.") entity types=".
                    count($this->__ENTITY_TYPES), SOD_DET);
        $mark = 0;
        for ($e = 1; $e < $ecount; $e++) {
            /* call the main routine for each entity level */
            $dcount = count($page->Entities);
            for ($d = $dcount -1; $d < $dcount; $d++) {

                $entity = $page->Entities[$d];
                if ($entity->Level == $e -1) {
                    $this->__parseRest($e, $entity->IndexNo, $PageNo);
                }; // if $level

            }; // for $d
            $mark = $dcount;
        }; // for $e

        $this->__setDzCols($PageNo);

        if (count($this->__SubPageSources)) {
            $file_name = dzGetInipath(DZID_FORM_SOURCE_FOLDER)."/".
                         array_shift($this->__SubPageSources);
            $this->__Cache->addPage();
            $this->__Source = &new soInputStream("file://".$file_name);
            $this->parse($PageNo +1);
        } else {
            /* Add cols to StructIni */
            $this->__Cache->StructIni =
                soJoinStr($this->__Cache->StructIni, "\n",
                          $this->__ColsIni->toString(NULL), 1);
            unset($this->__ColsIni);
            unset($this->__SubPageSources);
        };
        return $this->__Cache;
    } // parse


    /** Sorts out the dataxi block.
     *  Gets the <-- dataxi --> comment block from the HTML source and splits
     *  it to StructIni/FormOptionsIni properties.
     *  This routine only sets the properties for the 1st page and those
     *  that are common for all pages.
     *  Other page specific properties are handled by ____setAllPageProperties
     *  that is called from this function.
     *  @param int $PageNo Page no. to store page props to.
     */
    function __processDataxiBlock($PageNo = 0) {

        $cache = &$this->__Cache;
        $page = &$cache->Pages[$PageNo];

        /* look for main ini */
        $ini_src = str_replace(" ", "\n",
            dzCleanStructSource(
            soParseVal($this->__Source->Data, "<!-- dataxi", "-->", 1, 1)));
        if (soGetStreamType($ini_src) == "file") {
            $ini_src = dzGetIniPath(DZID_INI_FOLDER)."/".
                       soExtractVal($ini_src, "://", 2);
            $is = new soInputStream("file://".$ini_src, "");
            $ini_src = dzCleanStructSource($is->Data);
        }; // if
        $ini = new soIni($ini_src);

        /* The main page contains more information than other, like
         * the names of all possible sub pages */
        if (!$PageNo) {
            $this->__setCacheProperties($ini, $cache);

            $page_names = $ini->getValue(DZ_SYS_INISECTION, DZID_PAGES, 0);
            $this->__SubPageSources = soExplode($page_names, ",");
            $this->__PageSources = array_merge($this->__PageSources,
                                                  soExplode($page_names, ","));
        }; // if

        $this->__setPageProperties($ini, $page);
    } // __processDataxiBlock


    /** Sets properties for cache (common to all pages).
     *  Used by __processDataxiBlock to set properties for cache.
     *  @param soIni $Ini Ini object to look for properties from.
     *  @param dzSECache $Cache Current cache object
     */
    function __setCacheProperties($Ini, &$Cache) {

        /* Set dbalias from form or from app ini */
        $Cache->DbAlias = $Ini->getValue(DZ_SYS_INISECTION,
                                         DZID_DBALIAS, SOINI_NOSUB);
        if (!$Cache->DbAlias) {
            /* Use app default for dbalias when form does not specify one */
            $Cache->DbAlias = dzGetIniItem(DZIS_GENERAL, DZID_DBALIAS);
        }; // if

        /* Set database translation table name from form or from app ini */
        $Cache->DBTT = $Ini->getValue(DZ_SYS_INISECTION, DZID_DBTT,
                                      SOINI_NOSUB);
        if (!$Cache->DBTT) {
            /* Use app default for dbtt when form does not specify one */
            $Cache->DBTT = dzGetIniItem(DZIS_GENERAL, DZID_DBTT);
        }; // if

        /* dztables, dzcols and sorting goes to struct */
        $ini_arr = $Ini->toArray(array(DZ_TABLES_INISECTION));
        $Cache->StructIni = implode("\n", $ini_arr);
        $ini_arr = $Ini->toArray(array(DZ_SORTING_INISECTION));
        $Cache->StructIni = soJoinStr($Cache->StructIni, "\n",
                                      implode("\n", $ini_arr));
# XXX THIS NEEDS TO BE SORTED SOON !!!
# Dataxi block [dzcols] should be joined with the one gathered from
# the column setup.
#        $ini_arr = $Ini->toArray(array(DZ_COLUMNS_INISECTION));
#        $tmp_ini = new soIni(implode("\n", $ini_arr));
#        $Cache->StructIni = soJoinStr($Cache->StructIni, "\n",
#                                      $tmp_ini->toString(NULL, "\n"));

        /* interfaces section goes to cache */
        $ini_arr = $Ini->toArray(array(DZ_INTERFACES_INISECTION));
        $Cache->InterfacesIni = implode("\n", $ini_arr);

        /* other, list, stonga and event sections goes to form */
        $ini_arr = $Ini->toArray(array(DZ_OTHER_INISECTION));
        $Cache->FormOptionsIni = soJoinStr($Cache->FormOptionsIni, "\n",
                                           implode("\n", $ini_arr));

        $ini_arr = $Ini->toArray(array(DZ_LIST_INISECTION));
        if ($ini_arr == array()) $ini_arr = $this->__addKeysToListCols();
        $Cache->FormOptionsIni = soJoinStr($Cache->FormOptionsIni, "\n",
                                          implode("\n", $ini_arr));

        $ini_arr = $Ini->toArray(array(DZ_STONGA_INISECTION));
        $Cache->FormOptionsIni = soJoinStr($Cache->FormOptionsIni, "\n",
                                          implode("\n", $ini_arr));

        $ini_arr = $Ini->toArray(array(DZ_EVENT_INISECTION));
        $Cache->FormOptionsIni = soJoinStr($Cache->FormOptionsIni, "\n",
                                          implode("\n", $ini_arr));

    } // __setCacheProperties


    /** Sets properties for any page.
     *  Used by __processDataxiBlock to set properties for any page.
     *  @param soIni $Ini Ini object to look for properties from.
     *  @param dzSEPageCache $Page Current page object
     */
    function __setPageProperties($Ini, &$Page) {

        /* Check for group access */
        $value = $Ini->getValue(DZ_ACCESS_INISECTION, DZID_GROUPS,
                                SOINI_NOSUB);
        if ($value) $Page->UserGroups = soExplode($value, ",");

        /* Check for group access */
        $value = $Ini->getValue(DZ_ACCESS_INISECTION, DZID_GROUPS,
                                SOINI_NOSUB);
        if ($value) $Page->UserLevel = intval($value);

    } // __setPageProperties


    /** Returns keys cols as [list] section
     *  Used by __processDataxiBlock. This is called when there is no [list]
     *  section specified in the <!-- dataxi --> block.
     *  @return array/str Returns key column ini data for [list] section.
     */
    function __addKeysToListCols() {
        $ini = new soIni($this->__Cache->StructIni, "\n");
        if (!$ini->isSection(DZ_TABLES_INISECTION)) return array(); // <-- EXIT

        $set = $ini->getValue(DZ_TABLES_INISECTION, 0, NULL);
        if ($set == NULL) return array(); // <-- EXIT

        $table = $ini->getValueName(DZ_TABLES_INISECTION, 0);
        $result = soExplode($set->getItem(DZ_TABLE_KEY_INIKEY), ",");
        $this->Error = $set->Error;
        if (($set->Error) || ($result == array())) return array(); // <-- EXIT

        $icount = count($result);
        for ($i = 0; $i < $icount; $i++) {
            $result[$i] = $table.".".
                soExtractVal($result[$i], ":", 1)."={ len=10 }";
        }; // for $i

        soinsElement($result, "[".DZ_LIST_INISECTION."]", 0);
        return $result;
    } // __addKeysToListCols


    /** Sorts out the data column definitions.
     *  Loops through entities and looks for dz entities. The results are
     *  appended to StructIni (str) property.
     *  @param int $PageNo Page no. to store page props to.
     */
    function __setDzCols($PageNo = 0) {

        $cache = &$this->__Cache;
        $page = &$cache->Pages[$PageNo];

        $ini = &$this->__ColsIni;

        /* init a list for possibly translatable cell nos. */
        $page->TextCells = array();

        $ents = &$page->Entities;
        $tcount = count($page->CellNos);
        if ($tcount == 0) $this->Error = 1;
        soDebug("dzParser->__setDzCols: '".$tcount.
                "' entities to process", SOD_LO);
        for ($t = 0; $t < $tcount; $t++) {
            $pos = $page->CellNos[$t];

            /* Does it look like a dataz TD? It does so by starting with '#'
               and then ending at some point with "<" */
            $entity = $ents[$pos];
            $value = soSplitStr($entity->Value, "#", 1, 2, 0);

            /* ok, starts with "#" and is not the only character in the string
             */
            if ($value[1]) {
                $precode = $value[0];

                /* This looks pretty much dataz TD, so clean it up from crap.
                 * removed. And double spaces. And... */
                $value[1] = dzCleanStructSource($value[1]);

                if (soStrPos($value[1], "</") != -1) {
                    /* look for the first "</" */
                    $value = soSplitStr($value[1], "</", 1, 2, 0);
                    if (!count($value)) $value = array("", "");
                } else {
                    $value = array($value[1], "");
                };

                /* Looks very much like dzcol TD! */
                if ($value[0]) {
                    /* Now, what we do is take the definition and store the
                       remaining html code before and after it to PreValue and
                       PostValue.

                       The definition is in format:
                       #table.colname:datatype
                       struct_key=value
                       [control]
                       control_key=value
                       [format]
                       format_key=value
                     */
                    $entity->PreValue = $precode;
                    $entity->PostValue = $value[1];

                    /* Convert "\n" delimited string to an array and setup
                     * [alias.table] section to replace #alias.column */
                    $ini_array = explode("\n", $value[0]);
                    $ini_array[0] = substr($ini_array[0], 1);
                    $type_split = soSplitStr($ini_array[0], ":", 1, 0);
                    if (!count($type_split)) {
                        $type_split = array($ini_array[0], "");
                    }; // type_split
                    $entity->Name = $type_split[0];
                    $split = soSplitStr($entity->Name, ".", 1, 0);
                    if (!count($split)) {
                        $split = array(DZPSEUDO, $entity->Name);
                    }; // if split
                    $entity->Name = implode(".", $split);
                    if (!$entity->Name) {
                        soDebug("dzInHtml->__setDzCols: empty entity name '".
                                $ini_array[0]."'", SOD_HI);
                    }; // if
                    $ini_array[0] = "[".$type_split[0];
                    if ($type_split[1]) {
                        $ini_array[0] .= ":".$type_split[1];
                    }; // if
                    $ini_array[0] .= "]";

                    $ini_data = new soIni(implode("\n", $ini_array));

                    /* [control] section */
                    $options = $ini_data->toArray(array(DZCPT_CONTROL));
                    soDelElement($options, 0);
                    $page->Controls->setItem($entity->Name,
                                             implode("\n", $options));

                    /* [format] section */
                    $options = $ini_data->toArray(array(DZCPT_FORMAT));
                    soDelElement($options, 0);
                    $page->Formats->setItem($entity->Name,
                                            implode("\n", $options));

                    /* [group] section */
                    $is_group = $ini_data->isSection(DZCPT_GROUP);
                    if ($is_group) {

                        /* get or create new sect */
                        $grp_id = dzGetGroupID($PageNo, $entity->ParentNo);
                        $grp = $cache->Groups->getItem($grp_id);
                        if ($cache->Groups->Error) {
                            $grp = new dzGroup();
                            $grp->Type = DZGT_SUB;
                        }; // if

                        /* filtering */
                        $opt_str = $ini_data->getValue(DZCPT_GROUP,
                                                       DZCP_FILTER_SEED,
                                                       SOINI_NOSUB);
                        if ($opt_str) {
                            $grp->Filter->Seed = soExplode($opt_str, ",");
                        }; // if
                        $opt_str = $ini_data->getValue(DZCPT_GROUP,
                                                       DZCP_FILTER_POT,
                                                       SOINI_NOSUB);
                        if ($opt_str) {
                            $grp->Filter->Pot = soExplode($opt_str, ",");
                        }; // if

                        /* inserting */
                        $opt_str = $ini_data->getValue(DZCPT_GROUP,
                                                       DZCP_INSERT,
                                                       SOINI_NOSUB);
                        if (!in_array($opt_str,
                            array(DZGI_ONE_END,
                                  DZGI_ANY_END,
                                  DZGI_ANY_ANY,
                                  DZGI_ONE_ANY,
                                  DZGI_NONE))) {
                            $opt_str = DZGI_ANY_ANY;
                        }; // if
                        $grp->Inserting = $opt_str;

                        /* deleting */
                        $opt_str = $ini_data->getValue(DZCPT_GROUP,
                                                      DZCP_DELETE,
                                                      SOINI_NOSUB);
                        if (!in_array($opt_str,
                            array(DZGD_ONE_ANY, DZGD_NONE))) {
                            $opt_str = DZGD_ANY_ANY;
                        };
                        $grp->Deleting = $opt_str;

                        /* rowlimit */
                        $opt_str = $ini_data->getValue(DZCPT_GROUP,
                                                      DZCP_ROWLIMIT,
                                                      SOINI_NOSUB);
                        if ($opt_str) $grp->RowLimit = intval($opt_str);

                        /* This groupie thing also means, that the grand
                         * parent entity, the table, should have an option set
                         * to inform about this group. */
                        $ent = $entity;
                        $ent_no = 0;
                        while (($ent->Type != TABLE_ENTITY_TAG) &&
                               ($ent->ParentNo)) {
                            $ent_no = $ent->ParentNo;
                            $ent = $ents[$ent->ParentNo];
                        }; // while

                        if ($ent->Type == TABLE_ENTITY_TAG) {
                            $grp->ParentNo = $ent_no;
                        };

                        /* Save group */
                        $cache->Groups->setItem($grp_id, $grp);
                    }; // if group definitions

                    /* [struct] section -- actually [entityname]*/
                    $options = $ini_data->toArray(array(0));
                    $name = soParseVal($options[0], "[", "]", 1, 0);
                    $split = soSplitStr($name, ".", 1);
                    if (count($split) < 2) {
                        /* This was a pseudo column. Add the pseudo prefix */
                        $split = array(DZPSEUDO, $name);
                    } // if
                    $real_name = implode(".", $split);

                    $options[0] = soSwapStr($options[0], $name, $real_name, 1);
                    $mini_ini = new soIni(implode("\n", $options));
                    $vkeys = $mini_ini->getValueKeys(0);
                    if (!$vkeys) $vkeys = array();
                    $ini->setValue("", DZ_COLUMNS_INISECTION, $real_name);
                    foreach ($vkeys as $vkey) {
                        $prop = $mini_ini->getValue(0, $vkey, 0);
                        $ini->setValue($prop, DZ_COLUMNS_INISECTION,
                                       $real_name, $vkey);
                    }; // foreach

                    unset($entity->Value);

                    /* update index for entities in */
                    $page->DataCellNos[] = $pos;
                    $page->Entities[$pos] = $entity;

                    $parent_no = $this->__findParentTable($page, $pos);
                    if (!in_array($parent_no, $page->InnerTables)) {
                        $page->InnerTables[] = $parent_no;
                    }; // if
                    if ($is_group) {
                        $group = "group=".$PageNo.".".$entity->ParentNo;
                    };
                    soDebug("dzParser->__setDzCols: found '".
                            $entity->Name."' $group", SOD_DET);

                } else {; // if $value[0]
                    /* Nothing to do with dataxi TD, but might contain
                     * something translatable, so this entityno. must
                     * be stored for later viewing. */
                    $page->TextCells[] = $pos;
                }; // else if $value[0]
            } else {
                /* Nothing to do with dataxi TD, but might contain
                 * something translatable, so this entityno. must
                 * be stored for later viewing. */
                $page->TextCells[] = $pos;
            }; // if $value[1]
        }; // for $t

    } // __setDzCols


    /** Finds table element to which specified entity belongs.
     *  Returns an table element no. for specified element no. if any.
     *  Example, if you pass an element no. of a TD element, the function
     *  tries to find the parent element that holds the TABLE entity to
     *  which the TD belongs to.
     *  @param int $EntityNo Entitie no.
     *  @return int Returns parent table entity no. or -1 on error (practically
     *  bad entity no.)
     */
    function __findParentTable(&$Page, $EntityNo) {
        $entity = $Page->Entities[$EntityNo];
        if ($entity->Type == TABLE_ENTITY_TAG) {
            return $EntityNo;
        } else {
            if ($entity->ParentNo) {
                return $this->__findParentTable($Page, $entity->ParentNo);
            } else {
                return -1;
            }; // else if
        }; // else if
    } // __findParentTable


    /** Checks for certain tagtype in certain level.
     *  Checks through all the entities for tags in specified level.
     *
     *  This is not easy to explain, sorry. An example below should help a bit.
     *  It expects that entity types in level 1 are HEAD and BODY and element
     *  no 0 is the HTML entity.
     *
     *  Example: __findTag(1, 0, 0) would return array("HEAD", 1, 1)
     *      as there probably is not more than one head section.
     *
     *  @param int $Level Level (html, head/body, tr, td)
     *  @param int $ParentIndexNo Parent entity index no.
     *  @param int $PageNo Page no. to store page props to.
     *  @return array/any Returns an array of 3 items: 0=tag type, 1=no. of
     *      start tags and 2=no. of end tags in specified entity's value.
     */
    function __findTag($Level, $ParentIndexNo, $PageNo) {

        $cache = &$this->__Cache;
        $page = &$cache->Pages[$PageNo];

        $value = $page->Entities[$ParentIndexNo]->Value;

        $starts = 0;
        $tcount = count($this->__ENTITY_TYPES[$Level]);
        $first_arr = array();

        /** Find out the 1st tag type */
        $first_pos = -1;
        $first_tag = "";
        for ($t = 0; $t < $tcount; $t++) {
            $pos = soStrPos($value, $this->__ENTITY_TYPES[$Level][$t], 1);
            if ($pos != -1) {
                if (!$first_tag || ($first_pos > $pos)) {
                    $first_tag = $this->__ENTITY_TYPES[$Level][$t];
                    $first_pos = $pos;
                }; // if
            } // if $pos != -1
        }; // for $t

        if (!$first_tag) return -1;

        $starts = soStrCount($value, "<".$first_tag, 1);
        if (!$starts) return -1;

        $result = array($first_tag, $starts,
                        soStrCount($value, "</".$first_tag, 1));

        return $result;
    } // __findTag


    /** Gets level for specified tag.
     *  @param string $Type Open/Close tag (HTML, /TD, ...) to get a level for. Levels
     *      are declared in $this->__ENTITY_TYPES (see constructor for more).
     *      This function is used by parseRest().
     *  @return int Returns tag level or -1 if tag has not been specified.
     */
    function __getLevel($Type) {
        if (substr($Type, 0, 1) == "/") {
            $x = -1;
            $Type = substr($Type, 1);
        } else {
            $x = 1;
        };
        $level = count($this->__ENTITY_TYPES) -1;
        $found = 0;
        while ((!$found) && ($level > 0)) {
            $found = in_array(strtoupper($Type),
                              $this->__ENTITY_TYPES[$level--]);
        }; // while

        return $x *$level;
    } // __getLevel


    /** Parses the rest of the entities
     *  This is called by the parse().
     *  @param int $Level Level to parse (html, head/body, etc)
     *  @param int $ParentIndexNo Parent entity index no.
     *  @param int $PageNo Page no. to store page props to.
     */
    function __parseRest($Level, $ParentIndexNo, $PageNo) {

        $cache = &$this->__Cache;
        $page = &$cache->Pages[$PageNo];


        $parent = &$page->Entities[$ParentIndexNo];
        do { /* while parent->Value */

            $result = $this->__findTag($Level, $ParentIndexNo, $PageNo);
            $Type = $result[0];
            $starts = $result[1];
            $ends = $result[2];

            /* If start/end tag count does not match, then it really really bad */
            if ($ends != $starts) $this->Error = 1;

            /* If no $Type found or error occured, then stop.
             * This means that parsing is done, time to EXIT */
            if (!$starts || ($this->Error)) {
                return; // <-- EXIT
            };
            soDebug("dzParser->__parseRest: $ParentIndexNo ($Type)", SOD_DET);

            $entity = new dzSourceEntity();
            $entity->Type = $Type;
            $entity->Level = $Level;
            
            /* get precode */
            $rest = soSplitStr($parent->Value,
                               "<".$Type, 1, 1, 1);
            $entity->PreCode = substr($rest[0], 0,
                                      strlen($rest[0]) -strlen($Type));
            $rest = soSplitStr($rest[1], ">", 1, 0, 1);
            $entity->setProperties($rest[0]);

            /* If this is the 1st tablem then set the alignment for the page */
            if (($Level == 2) && ($page->Align == -1)) {
                $align = $entity->getProperty("align");
                $page->Align = $align;
            };

            /* skip other & nested $Type blocks */
            $indent = 1;
            $no = 1;

            do {
                $rest = soSplitStr($rest[1], "<", $no, 2, 0);
                $no = 2;

                if ($rest[0]) $entity->Value .= $rest[0];

                if (strtoupper(substr($rest[1], 1, strlen($Type))) ==
                    strtoupper($Type)) {
                    $indent++;
                } else {
                    if (strtoupper(substr($rest[1], 1, strlen($Type) +1)) ==
                        "/".strtoupper($Type)) {
                        $indent--;
                    };
                };

            } while ($indent);

            /* get postcode */
            if (soLocateStr($rest[1], "<", 2, 0) != -1) {
                $rest = soSplitStr($rest[1], "<", 2, 2, 0);
                $entity->PostCode = $rest[0];
                $parent->Value = $rest[1];
            } else {
                $entity->PostCode = $rest[1];
                $parent->Value = "";
            };

            $entity->ParentNo = $page->Entities[$ParentIndexNo]->IndexNo;
            $entity->IndexNo = $this->__NextIndexNo++;
            $page->Entities[$ParentIndexNo]->Children[] = $entity->IndexNo;
            /*
            if ($entity->Type == TH_ENTITY_TAG) {
                $color = dzGetIniItem(DZCL_SECTION, DZCL_HEADER_BG);
                if ($color) {
                    $entity->setProperty("bgcolor", $color);
                }; // if $color
            }; // if th
            */
            $page->Entities[] = $entity;

            if (($entity->Type == TD_ENTITY_TAG) ||
                ($entity->Type == TH_ENTITY_TAG)) {
                $page->CellNos[] = $entity->IndexNo;
            }; // if

            if ($entity->Value) {
                if ($Level == TD_LEVEL) {
                    $tmp_level = TABLE_LEVEL;
                } else {
                    $tmp_level = $Level +1;
                };
                $this->__parseRest($tmp_level, $entity->IndexNo, $PageNo);
            };

        } while ($parent->Value);

    } // __parseRest


}; // dzParser

?>
